| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- 'use client';
- import { Stage } from '@/components/stage';
- import { ThemeProvider } from '@/lib/hooks/use-theme';
- import { useStageStore } from '@/lib/store';
- import { loadImageMapping } from '@/lib/utils/image-storage';
- import { useEffect, useRef, useState, useCallback } from 'react';
- import { useParams } from 'next/navigation';
- import { useSceneGenerator } from '@/lib/hooks/use-scene-generator';
- import { useMediaGenerationStore } from '@/lib/store/media-generation';
- import { useWhiteboardHistoryStore } from '@/lib/store/whiteboard-history';
- import { createLogger } from '@/lib/logger';
- import { MediaStageProvider } from '@/lib/contexts/media-stage-context';
- import { generateMediaForOutlines } from '@/lib/media/media-orchestrator';
- const log = createLogger('Classroom');
- export default function ClassroomDetailPage() {
- const params = useParams();
- const classroomId = params?.id as string;
- const { loadFromStorage } = useStageStore();
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const generationStartedRef = useRef(false);
- const { generateRemaining, retrySingleOutline, stop } = useSceneGenerator({
- onComplete: () => {
- log.info('[Classroom] All scenes generated');
- },
- });
- const loadClassroom = useCallback(async () => {
- try {
- await loadFromStorage(classroomId);
- // If IndexedDB had no data, try server-side storage (API-generated classrooms)
- if (!useStageStore.getState().stage) {
- log.info('No IndexedDB data, trying server-side storage for:', classroomId);
- try {
- const res = await fetch(`/api/classroom?id=${encodeURIComponent(classroomId)}`);
- if (res.ok) {
- const json = await res.json();
- if (json.success && json.classroom) {
- const { stage, scenes } = json.classroom;
- useStageStore.getState().setStage(stage);
- useStageStore.setState({
- scenes,
- currentSceneId: scenes[0]?.id ?? null,
- });
- log.info('Loaded from server-side storage:', classroomId);
- // Hydrate server-generated agents into IndexedDB + registry.
- // Don't set selectedAgentIds here — the general agent
- // restoration logic below (Path 2) handles it uniformly.
- if (stage.generatedAgentConfigs?.length) {
- const { saveGeneratedAgents } = await import('@/lib/orchestration/registry/store');
- await saveGeneratedAgents(stage.id, stage.generatedAgentConfigs);
- log.info('Hydrated server-generated agents for stage:', stage.id);
- }
- }
- }
- } catch (fetchErr) {
- log.warn('Server-side storage fetch failed:', fetchErr);
- }
- }
- // Restore completed media generation tasks from IndexedDB
- await useMediaGenerationStore.getState().restoreFromDB(classroomId);
- // Restore agents for this stage
- const { loadGeneratedAgentsForStage, useAgentRegistry } =
- await import('@/lib/orchestration/registry/store');
- const generatedAgentIds = await loadGeneratedAgentsForStage(classroomId);
- const { useSettingsStore } = await import('@/lib/store/settings');
- if (generatedAgentIds.length > 0) {
- // Auto mode — use generated agents from IndexedDB
- useSettingsStore.getState().setAgentMode('auto');
- useSettingsStore.getState().setSelectedAgentIds(generatedAgentIds);
- } else {
- // Preset mode — restore agent IDs saved in the stage at creation time.
- // Filter out any stale generated IDs that may have been persisted before
- // the bleed-fix, so they don't resolve against a leftover registry entry.
- const stage = useStageStore.getState().stage;
- const stageAgentIds = stage?.agentIds;
- const registry = useAgentRegistry.getState();
- const cleanIds = stageAgentIds?.filter((id) => {
- const a = registry.getAgent(id);
- return a && !a.isGenerated;
- });
- useSettingsStore.getState().setAgentMode('preset');
- useSettingsStore
- .getState()
- .setSelectedAgentIds(
- cleanIds && cleanIds.length > 0 ? cleanIds : ['default-1', 'default-2', 'default-3'],
- );
- }
- } catch (error) {
- log.error('Failed to load classroom:', error);
- setError(error instanceof Error ? error.message : 'Failed to load classroom');
- } finally {
- setLoading(false);
- }
- }, [classroomId, loadFromStorage]);
- useEffect(() => {
- // Reset loading state on course switch to unmount Stage during transition,
- // preventing stale data from syncing back to the new course
- setLoading(true);
- setError(null);
- generationStartedRef.current = false;
- // Clear previous classroom's media tasks to prevent cross-classroom contamination.
- // Placeholder IDs (gen_img_1, gen_vid_1) are NOT globally unique across stages,
- // so stale tasks from a previous classroom would shadow the new one's.
- const mediaStore = useMediaGenerationStore.getState();
- mediaStore.revokeObjectUrls();
- useMediaGenerationStore.setState({ tasks: {} });
- // Clear whiteboard history to prevent snapshots from a previous course leaking in.
- useWhiteboardHistoryStore.getState().clearHistory();
- loadClassroom();
- // Cancel ongoing generation when classroomId changes or component unmounts
- return () => {
- stop();
- };
- }, [classroomId, loadClassroom, stop]);
- // Auto-resume generation for pending outlines
- useEffect(() => {
- if (loading || error || generationStartedRef.current) return;
- const state = useStageStore.getState();
- const { outlines, scenes, stage } = state;
- // Check if there are pending outlines
- const completedOrders = new Set(scenes.map((s) => s.order));
- const hasPending = outlines.some((o) => !completedOrders.has(o.order));
- if (hasPending && stage) {
- generationStartedRef.current = true;
- // Load generation params from sessionStorage (stored by generation-preview before navigating)
- const genParamsStr = sessionStorage.getItem('generationParams');
- const params = genParamsStr ? JSON.parse(genParamsStr) : {};
- // Reconstruct imageMapping from IndexedDB using pdfImages storageIds
- const storageIds = (params.pdfImages || [])
- .map((img: { storageId?: string }) => img.storageId)
- .filter(Boolean);
- loadImageMapping(storageIds).then((imageMapping) => {
- generateRemaining({
- pdfImages: params.pdfImages,
- imageMapping,
- stageInfo: {
- name: stage.name || '',
- description: stage.description,
- language: stage.language,
- style: stage.style,
- },
- agents: params.agents,
- userProfile: params.userProfile,
- });
- });
- } else if (outlines.length > 0 && stage) {
- // All scenes are generated, but some media may not have finished.
- // Resume media generation for any tasks not yet in IndexedDB.
- // generateMediaForOutlines skips already-completed tasks automatically.
- generationStartedRef.current = true;
- generateMediaForOutlines(outlines, stage.id).catch((err) => {
- log.warn('[Classroom] Media generation resume error:', err);
- });
- }
- }, [loading, error, generateRemaining]);
- return (
- <ThemeProvider>
- <MediaStageProvider value={classroomId}>
- <div className="h-screen flex flex-col overflow-hidden">
- {loading ? (
- <div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
- <div className="text-center text-muted-foreground">
- <p>Loading classroom...</p>
- </div>
- </div>
- ) : error ? (
- <div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
- <div className="text-center">
- <p className="text-destructive mb-4">Error: {error}</p>
- <button
- onClick={() => {
- setError(null);
- setLoading(true);
- loadClassroom();
- }}
- className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
- >
- Retry
- </button>
- </div>
- </div>
- ) : (
- <Stage onRetryOutline={retrySingleOutline} />
- )}
- </div>
- </MediaStageProvider>
- </ThemeProvider>
- );
- }
|